package com.abso.sunlight.api;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

/**
 * Legislator's parameter tag.
 */
@Retention(RetentionPolicy.RUNTIME)
@interface LegislatorParam {

    /**
     * Returns the XML field name.
     * 
     * @return the XML field name.
     */
    public String value();
}

/**
 * Holds information on a query able to return a set of legislators.
 */
public class LegislatorQuery {

    /** The title held by this legislator. */
    @LegislatorParam("title")
    private List<Title> titleList = new ArrayList<Title>();

    /** The legislator's first name. */
    @LegislatorParam("firstname")
    private List<String> firstNameList = new ArrayList<String>();

    /** The legislator's middle name or initial. */
    @LegislatorParam("middlename")
    private List<String> middleNameList = new ArrayList<String>();

    /** The legislator's last name. */
    @LegislatorParam("lastname")
    private List<String> lastNameList = new ArrayList<String>();

    /** The legislator's suffix (Jr., III, etc.). */
    @LegislatorParam("name_suffix")
    private List<String> nameSuffixList = new ArrayList<String>();

    /** The preferred nickname of legislator (if any). */
    @LegislatorParam("nickname")
    private List<String> nicknameList = new ArrayList<String>();

    /** The legislator's political party. */
    @LegislatorParam("party")
    private List<Party> partyList = new ArrayList<Party>();

    /** The legislator's state. */
    @LegislatorParam("state")
    private List<State> stateList = new ArrayList<State>();

    /** If legislator is a representative, their district. 0 is used for At-Large districts. */
    @LegislatorParam("district")
    private List<String> districtList = new ArrayList<String>();

    /** If legislator is currently serving or if legislator is no longer in office due to defeat/resignation/death/etc.. */
    @LegislatorParam("in_office")
    private List<Boolean> inOfficeList = new ArrayList<Boolean>();

    /** The legislator's gender. */
    @LegislatorParam("gender")
    private List<Gender> genderList = new ArrayList<Gender>();

    /** The congressional office phone number. */
    @LegislatorParam("phone")
    private List<String> phoneList = new ArrayList<String>();

    /** The congressional office fax number. */
    @LegislatorParam("fax")
    private List<String> faxList = new ArrayList<String>();

    /** The URL of Congressional website. */
    @LegislatorParam("website")
    private List<String> webSiteList = new ArrayList<String>();

    /** The URL of web contact form. */
    @LegislatorParam("webform")
    private List<String> webContactFormList = new ArrayList<String>();

    /** The legislator's email address (if known). */
    @LegislatorParam("email")
    private List<String> emailList = new ArrayList<String>();

    /** The legislator's Washington DC Office Address. */
    @LegislatorParam("congress_office")
    private List<String> congressOfficeList = new ArrayList<String>();

    /** The legislator ID assigned by Congressional Biographical Directory (also used by Washington Post/NY Times). */
    @LegislatorParam("bioguide_id")
    private List<String> bioguideIdList = new ArrayList<String>();

    /** The legislator ID assigned by Project Vote Smart. */
    @LegislatorParam("votesmart_id")
    private List<String> voteSmartIdList = new ArrayList<String>();

    /** The Federal Election Commission ID. */
    @LegislatorParam("fec_id")
    private List<String> fecIdList = new ArrayList<String>();

    /** The ID assigned by GovTrack.us. */
    @LegislatorParam("govtrack_id")
    private List<String> govTrackIdList = new ArrayList<String>();

    /** The ID provided by Center for Responsive Politics. */
    @LegislatorParam("crp_id")
    private List<String> crpIdList = new ArrayList<String>();

    /** The performer ID on eventful.com. */
    @LegislatorParam("eventful_id")
    private List<String> eventfulIdList = new ArrayList<String>();

    /** The ID used by deprecated Sunlight Labs API (should not be used except for legacy purposes). */
    @LegislatorParam("sunlight_old_id")
    private List<String> sunlightOldIdList = new ArrayList<String>();

    /** The URL of Legislator's entry on Congresspedia. */
    @LegislatorParam("congresspedia_url")
    private List<String> congresspediaURLList = new ArrayList<String>();

    /** The Congressperson's official Twitter account. */
    @LegislatorParam("twitter_id")
    private List<String> twitterIdList = new ArrayList<String>();

    /** The Congressperson's official Youtube account. */
    @LegislatorParam("youtube_url")
    private List<String> youTubeURLList = new ArrayList<String>();

    /**
     * Gets the titles held by this legislator.
     * 
     * @return the list of titles.
     */
    public List<Title> getTitleList() {
        return titleList;
    }

    /**
     * Gets the list of legislator's first names.
     * 
     * @return the list of first names.
     */
    public List<String> getFirstNameList() {
        return firstNameList;
    }

    /**
     * Gets the list of legislator's middle names.
     * 
     * @return the list of middle names.
     */
    public List<String> getMiddleNameList() {
        return middleNameList;
    }

    /**
     * Gets the list of legislator's last names.
     * 
     * @return the list of last names.
     */
    public List<String> getLastNameList() {
        return lastNameList;
    }

    /**
     * Gets the list of legislator's name suffixes.
     * 
     * @return the list of name suffixes.
     */
    public List<String> getNameSuffixList() {
        return nameSuffixList;
    }

    /**
     * Gets the list of legislator's nicknames.
     * 
     * @return the list of nicknames.
     */
    public List<String> getNicknameList() {
        return nicknameList;
    }

    /**
     * Gets the political parties to search for.
     * 
     * @return the list of political parties.
     */
    public List<Party> getPartyList() {
        return partyList;
    }

    /**
     * Gets the list of states.
     * 
     * @return the list of states.
     */
    public List<State> getStateList() {
        return stateList;
    }

    /**
     * Gets the districts of representatives. 0 is used for At-Large districts.
     * 
     * @return the districts.
     */
    public List<String> getDistrictList() {
        return districtList;
    }

    /**
     * Returns the list of active/inactive flags to search for.
     * 
     * @return the list of active/inactive flags.
     */
    public List<Boolean> getInOfficeList() {
        return inOfficeList;
    }

    /**
     * Gets the genders to search for.
     * 
     * @return the list of genders.
     */
    public List<Gender> getGenderList() {
        return genderList;
    }

    /**
     * Returns the list of congressional office phone numbers.
     * 
     * @return the list of phone numbers.
     */
    public List<String> getPhoneList() {
        return phoneList;
    }

    /**
     * Returns the list of congressional office fax numbers.
     * 
     * @return the list of fax numbers.
     */
    public List<String> getFaxList() {
        return faxList;
    }

    /**
     * Returns the list of URLs of congressional Web sites.
     * 
     * @return the list of Web site URLs.
     */
    public List<String> getWebSiteList() {
        return webSiteList;
    }

    /**
     * Returns the list of URLs of Web contact forms.
     * 
     * @return the list of Web contact form URLs.
     */
    public List<String> getWebContactFormList() {
        return webContactFormList;
    }

    /**
     * Returns the list of email addresses.
     * 
     * @return the list of email addresses.
     */
    public List<String> getEmailList() {
        return emailList;
    }

    /**
     * Returns the list of Washington DC office addresses.
     * 
     * @return the list of Washington DC office addresses.
     */
    public List<String> getCongressOfficeList() {
        return congressOfficeList;
    }

    /**
     * Returns the IDs assigned by Congressional Biographical Directory (also used by Washington Post/NY Times).
     * 
     * @return the IDs of legislators.
     */
    public List<String> getBioguideIdList() {
        return bioguideIdList;
    }

    /**
     * Returns the IDs assigned by Project Vote Smart.
     * 
     * @return the IDs assigned by Project Vote Smart.
     */
    public List<String> getVoteSmartIdList() {
        return voteSmartIdList;
    }

    /**
     * Returns the IDs assigned by the Federal Election Commission ID.
     * 
     * @return the IDs assigned by the Federal Election Commission ID.
     */
    public List<String> getFecIdList() {
        return fecIdList;
    }

    /**
     * Returns the IDs assigned by GovTrack.us.
     * 
     * @return the IDs assigned by GovTrack.us.
     */
    public List<String> getGovTrackIdList() {
        return govTrackIdList;
    }

    /**
     * Returns the IDs provided by Center for Responsive Politics.
     * 
     * @return the IDs provided by Center for Responsive Politics.
     */
    public List<String> getCrpIdList() {
        return crpIdList;
    }

    /**
     * Returns the performer IDs on eventful.com.
     * 
     * @return the performer IDs on eventful.com.
     */
    public List<String> getEventfulIdList() {
        return eventfulIdList;
    }

    /**
     * Returns the IDs used by deprecated Sunlight Labs API (should not be used except for legacy purposes).
     * 
     * @return the IDs used by deprecated Sunlight Labs API (should not be used except for legacy purposes).
     */
    public List<String> getSunlightOldIdList() {
        return sunlightOldIdList;
    }

    /**
     * Returns the URLs of entries on Congresspedia.
     * 
     * @return the URLs of entries on Congresspedia.
     */
    public List<String> getCongresspediaURLList() {
        return congresspediaURLList;
    }

    /**
     * Returns the Congresspersons' official Twitter accounts.
     * 
     * @return the Congresspersons' official Twitter accounts.
     */
    public List<String> getTwitterIdList() {
        return twitterIdList;
    }

    /**
     * Returns the Congresspersons' official YouTube accounts.
     * 
     * @return the Congresspersons' official YouTube accounts.
     */
    public List<String> getYouTubeURLList() {
        return youTubeURLList;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
    }

    /**
     * Returns the current set of parameters' values.
     * 
     * @return the current set of parameters' values.
     * @throws SunlightException
     *             if an exception occurred accessing the parameters' fields.
     */
    @SuppressWarnings("unchecked")
    public Map<String, String[]> getParameters() throws SunlightException {
        Map<String, String[]> params = new HashMap<String, String[]>();
        for (Field field : LegislatorQuery.class.getDeclaredFields()) {
            LegislatorParam param = field.getAnnotation(LegislatorParam.class);
            if (param != null) {
                String paramName = param.value();
                Object value = null;
                try {
                    value = field.get(this);
                } catch (IllegalArgumentException e) {
                    throw new SunlightException(e);
                } catch (IllegalAccessException e) {
                    throw new SunlightException(e);
                }
                if (value instanceof List) {
                    List<?> values = (List<?>) value;
                    if (!values.isEmpty()) {
                        if (values.get(0) instanceof String) {
                            params.put(paramName, values.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
                        } else if (values.get(0) instanceof Title) {
                            List<String> stringValues = new ArrayList<String>(values.size());
                            for (Title title : ((List<Title>) values)) {
                                stringValues.add(title.getAbbreviation());
                            }
                            params.put(paramName, stringValues.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
                        } else if (values.get(0) instanceof Gender) {
                            List<String> stringValues = new ArrayList<String>(values.size());
                            for (Gender gender : ((List<Gender>) values)) {
                                stringValues.add(gender.getAbbreviation());
                            }
                            params.put(paramName, stringValues.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
                        } else if (values.get(0) instanceof State) {
                            List<String> stringValues = new ArrayList<String>(values.size());
                            for (State state : ((List<State>) values)) {
                                stringValues.add(state.getAbbreviation());
                            }
                            params.put(paramName, stringValues.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
                        } else if (values.get(0) instanceof Party) {
                            List<String> stringValues = new ArrayList<String>(values.size());
                            for (Party party : ((List<Party>) values)) {
                                stringValues.add(party.getAbbreviation());
                            }
                            params.put(paramName, stringValues.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
                        } else if (values.get(0) instanceof Boolean) {
                            List<String> stringValues = new ArrayList<String>(values.size());
                            for (Boolean boolValue : ((List<Boolean>) values)) {
                                stringValues.add(boolValue.booleanValue() ? "1" : "0");
                            }
                            params.put(paramName, stringValues.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
                        }
                    }
                }
            }
        }
        if (params.get("in_office") == null) {
            params.put("all_legislators", new String[] { "1" });
        }
        return params;
    }

    /**
     * Transforms the current query into a XML representation.
     * 
     * @return the XML document representing this query.
     * @throws SunlightException
     *             if an exception occurred accessing the fields.
     */
    public Document toXML() throws SunlightException {
        Document doc = DocumentHelper.createDocument();
        Element queryElem = doc.addElement("legislator_query");
        Map<String, String[]> params = getParameters();
        List<String> paramNames = new ArrayList<String>(params.keySet());
        Collections.sort(paramNames);
        for (String paramName : paramNames) {
            String[] values = params.get(paramName);
            for (String value : values) {
                queryElem.addElement(paramName).setText(value);
            }
        }
        return doc;
    }

    /**
     * Preloads the current query from a XML document.
     * 
     * @param queryElem
     *            the XML document storing the parameters' values.
     * @throws SunlightException
     *             if an exception occurred accessing the fields.
     */
    @SuppressWarnings("unchecked")
    public void loadFromXML(Element queryElem) throws SunlightException {
        for (Field field : LegislatorQuery.class.getDeclaredFields()) {
            LegislatorParam param = field.getAnnotation(LegislatorParam.class);
            if (param != null) {
                String paramName = param.value();
                Object value = null;
                try {
                    value = field.get(this);
                } catch (IllegalArgumentException e) {
                    throw new SunlightException(e);
                } catch (IllegalAccessException e) {
                    throw new SunlightException(e);
                }
                if (value instanceof List) {
                    List<Object> values = (List<Object>) value;
                    values.clear();
                    for (Iterator<?> i = queryElem.elementIterator(paramName); i.hasNext();) {
                        String textValue = ((Element) i.next()).getText();
                        ParameterizedType listType = (ParameterizedType) field.getGenericType();
                        Type[] args = listType.getActualTypeArguments();
                        if (args.length > 0) {
                            Type listItemType = args[0];
                            if (Title.class == listItemType) {
                                values.add(Title.valueOfAbbreviation(textValue));
                            } else if (Gender.class == listItemType) {
                                values.add(Gender.valueOfAbbreviation(textValue));
                            } else if (Party.class == listItemType) {
                                values.add(Party.valueOfAbbreviation(textValue));
                            } else if (State.class == listItemType) {
                                values.add(State.valueOfAbbreviation(textValue));
                            } else if (Boolean.class == listItemType) {
                                values.add(textValue.equals("1") ? Boolean.TRUE : Boolean.FALSE);
                            } else if (String.class == listItemType) {
                                values.add(textValue);
                            }
                        }
                    }
                }
            }
        }
    }

}
